Skip to content

Enable and use ML-KEM by default#9732

Open
Frauschi wants to merge 3 commits intowolfSSL:masterfrom
Frauschi:pqc_first
Open

Enable and use ML-KEM by default#9732
Frauschi wants to merge 3 commits intowolfSSL:masterfrom
Frauschi:pqc_first

Conversation

@Frauschi
Copy link
Contributor

@Frauschi Frauschi commented Feb 2, 2026

This PR enables the standardized PQC algorithm ML-KEM (FIPS203) by default.

ML-KEM enabled by default

The ML-KEM algorithm is now enabled by default when building the library. This results in the same behavior as currently passing --enable-mlkem=yes to configure. When a specialized configuration for ML-KEM is required, --enable-mlkem=<desired options> is still possible as normal.

Algorithm changes

Prior to this PR, when ML-KEM has been enabled, all available groups that “contain” ML-KEM have been enabled to be used for the TLS key exchange. This behavior is now changed to the following:

Groups enabled by default: X25519MLKEM768, SECP256R1MLKEM768, SECP384R1MLKEM1024; these are the to-be-standardized hybrid PQ/T combinations from draft-ietf-tls-ecdhe-mlkem. These hybrids are considered “most secure” by the community and are already widely deployed in the web, especially X25519MLKEM768 (e.g., see here).

Currently not enabled by default: all standalone ML-KEM groups (MLKEM512, MLKEM768, MLKEM1024 from draft-ietf-tls-mlkem), as standalone ML-KEM usage is not widely deployed presently and also considered “more dangerous”.

Some “extra” hybrid PQ/T groups with different combinations of ML-KEM and ECC groups, which are defined by the OQS project, are also disabled by default (SECP256R1MLKEM512, SECP384R1MLKEM768, SECP521R1MLKEM1024, X25519MLKEM512).

Next to the default enablement of these groups, the “ preference “ of these groups has also been increased. On the client side, if no specific group for the key share is set, X25519MLKEM768 is now selected as the default one when Curve25519 is enabled. Otherwise, SECP384R1MLKEM1024 is used (if hybrids are not disabled). On the server side, when a client presents more than one key share in its ClientHello, a hybrid one is now also preferred to a traditional-only one.

This now also reflects the behavior of OpenSSL since version 3.5.

Build system changes

To reflect the changes from above, changes in the build system have been performed.

In the autoconf setup, --enable-mlkem is now set to yes by default. Furthermore, the following three new options are added:

  • --enable-pqc-hybrids (on by default): This enables (or disables) the three to-be-standardized hybrid PQ/T combinations.
  • --enable-tls-mlkem-standalone (off by default): This enables the standalone usage of ML-KEM. When enabled, the three standalone groups are advertised in the SupportedGroups extension by the client. When the hybrid groups from above are disabled, then MLKEM1024 is also selected as the default in the key share sent by the client. Otherwise, the hybrids are still ranked higher.
  • --enable-extra-pqc-hybrids (off by default, requires --enable-experimental in addition): This enables the extra PQ/T groups from the OQS project.

In addition to these new options, some dependencies of ML-KEM are now also enabled by default, namely SHA3 (--enable-sha3), SHAKE128 (--enable-shake128) and SHAKE256 (--enable-shake128).

Furthermore, when DTLS 1.3 is enabled, then ClientHello fragmentation (--enable-dtls-frag-ch) is now also enabled by default, as most PQ/T and standalone PQC key shares require fragmentation.

All these options and changes have also been applied to CMake builds and to Zephyr, too.

Runtime changes

The order of the algorithms has been changed in both the SupportedGroups extension sent by the client as well as in the preferredGroup array to rank the three PQ/T hybrids highest. The standalone ML-KEM groups are added based on their achieved security strength into the list, but always before their matching traditional counterparts (to prioritize the PQC groups over traditional ones in case hybrids are disabled).

For DTLS 1.3, the server now also automatically enables ClientHello fragmentation when the client sends a PQ/T or PQC key share (which requires a HelloRetryRequest and a second ClientHello).

For async crypto support, hybrid PQ/T groups are now also handled properly on both the client and server sides (only the ECC part of the hybrid combination is handled asynchronously, as ML-KEM currently lacks async support).

Tests

In the test infrastructure, all the new options are added to be tested. Furthermore, existing tests have been adapted to incorporate the different resource usage for the ML-KEM groups (more memory usage for static memory cases, different fragmentation in case of DTLS, manually set specific groups that are required in some tests, ...).

Moreover, the general testing of PQC and the PQ/T hybrids has been extended to cover all groups and more edge-cases.

Other changes

  • The examples have been updated to incorporate these changes.
  • The two ML-KEM source files wc_mlkem.c and wc_mlkem_poly.c have been updated to fix -Wconversion warnings (now also tested in CI for ML-KEM).
  • When RNG is disabled (--disable-rng), ML-KEM is now usable only with limited functionality. Mainly, key generation and encapsulation are only possible with a given seed. This is also handled in the unit tests.

@Frauschi Frauschi requested review from anhu, dgarske and douzzer February 2, 2026 16:10
@Frauschi Frauschi force-pushed the pqc_first branch 10 times, most recently from cb08c65 to 0040099 Compare February 4, 2026 14:45
Copy link
Contributor

@dgarske dgarske left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI is unhappy with some of the implicit casts I think:

                 from wolfcrypt/src/wc_mlkem.c:66:
wolfcrypt/src/wc_mlkem.c: In function 'wc_MlKemKey_MakeKeyWithRandom':
wolfcrypt/src/wc_mlkem.c:519:53: error: conversion to 'long unsigned int' from 'int' may change the sign of the result [-Werror=sign-conversion]
  519 |         e = (sword16*)XMALLOC((k + 1) * k * MLKEM_N * sizeof(sword16),
      |                                                     ^
./wolfssl/wolfcrypt/types.h:790:33: note: in definition of macro 'XMALLOC'
  790 |                 wolfSSL_Malloc((s)))
      |                                 ^
wolfcrypt/src/wc_mlkem.c:560:22: error: conversion from 'int' to 'byte' {aka 'unsigned char'} may change value [-Werror=conversion]
  560 |             buf[0] = k;
      |                      ^
wolfcrypt/src/wc_mlkem.c: In function 'mlkemkey_encapsulate':
wolfcrypt/src/wc_mlkem.c:852:42: error: conversion to 'int' from 'unsigned int' may change the sign of the result [-Werror=sign-conversion]
  852 |         ret = mlkem_get_noise(&key->prf, k, y, e1, e2, r);
      |                                          ^

@Frauschi
Copy link
Contributor Author

Frauschi commented Feb 5, 2026

CI is unhappy with some of the implicit casts I think:

Yeah I already talked to @SparkiDev about these and I still have to fix them (trying to fix the other failing tests first). The ML-KEM source files haven't yet been under test with these conversion checks.

@Frauschi Frauschi force-pushed the pqc_first branch 11 times, most recently from 5e3b1e9 to 644f303 Compare February 11, 2026 15:13
@Frauschi Frauschi force-pushed the pqc_first branch 2 times, most recently from 9e0f2fc to 9124900 Compare February 13, 2026 11:22
@dgarske dgarske assigned Frauschi and unassigned julek-wolfssl Feb 16, 2026
@dgarske dgarske removed their request for review February 17, 2026 23:29
@Frauschi Frauschi force-pushed the pqc_first branch 3 times, most recently from 6657e7d to def2516 Compare February 20, 2026 14:53
@Frauschi
Copy link
Contributor Author

I added a commit to incorporate the feedback of @SparkiDev as well as one to add test coverage for #9777.

@Frauschi Frauschi requested a review from SparkiDev February 20, 2026 19:32
@Frauschi Frauschi assigned SparkiDev and unassigned Frauschi Feb 20, 2026
@Frauschi
Copy link
Contributor Author

Jenkins retest this please - logs gone

@dgarske
Copy link
Contributor

dgarske commented Feb 23, 2026

Jenkins retest this please

* Enable ML-KEM by default
* Only allow three to-be-standardized hybrid PQ/T combinations by
  default
* Use X25519MLKEM768 as the default KeyShare in the ClientHello (if user
  does not override that)
* Disable standalone ML-KEM in supported groups by default (enable with
  --enable-tls-mlkem-standalone)
* Disable extra OQS-based hybrid PQ/T curves by default and gate
  behind --enable-experimental (enable with --enable-extra-pqc-hybrids)
* Reorder the SupportedGroups extension to reflect the preferences
* Reorder the preferredGroup array to also reflect the same preferences
* Enable DTLS1.3 ClientHello fragmentation by default when both DTLS1.3
  and ML-KEM are enabled
* Fix memory leak in TLS server PQC handling in case of ECH
* Ensure PQ/T hybrids are properly tested in unit tests
* Increase static memory buffer sizes to account for ML-KEM heap usage
* Add async support for ML-KEM hybrids
Also fix minor problems found with these tests
* fix -Wconversion warnings
* allow APIs without RNG usage in case WC_NO_RNG is defined
@Frauschi
Copy link
Contributor Author

I squashed the last three commits regarding the ML-KEM Wconversion fixes after the review of those. Thanks @SparkiDev for all the feedback!

@Frauschi
Copy link
Contributor Author

@julek-wolfssl @anhu @dgarske can you please review?

@Frauschi Frauschi requested a review from dgarske February 24, 2026 19:48
Comment on lines -4515 to +4517
#if defined(HAVE_PQC) && defined(WOLFSSL_DTLS13) && \
!defined(WOLFSSL_DTLS_CH_FRAG)
#warning "Using DTLS 1.3 + pqc without WOLFSSL_DTLS_CH_FRAG will probably" \
"fail.Use --enable-dtls-frag-ch to enable it."
#if defined(HAVE_PQC) && defined(WOLFSSL_HAVE_MLKEM) && \
defined(WOLFSSL_DTLS13) && !defined(WOLFSSL_DTLS_CH_FRAG)
#define WOLFSSL_DTLS_CH_FRAG
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WOLFSSL_DTLS_CH_FRAG is our hack to get PQC working with DTLS. I'm not sure that quietly enabling it is a good idea. It may clash with implementations that solve large key shares differently (eg. jumbo packets, IP fragmentation). @rizlik thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's OK: on the client-side we fully respect the protocol and on the server-side we are slightly less pickier compared to not having that enabled, but still on the safe side.

Even with option enabled, the server will still refuse messages that are valid per RFC, but impossible to handle statelessly: CH0 fragmented or CH1 fragmented but with cookie extension on the second fragment.

In the future we might want to allow users to drop statelessness for performance (removing both the needs of an extra round with HRR and/or interop (CH fragmented without having the cookie extension in the first fragment from other implementations). But for now, I don't see problems enabling it. We would fail interop with library that do not respect our model regardless.

Comment on lines +29105 to +29107
/* Re-enable HRR cookie to account for potential CH fragmentation */
ExpectIntEQ(wolfSSL_send_hrr_cookie(ssl_s, NULL, 0), WOLFSSL_SUCCESS);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't promote changing settings in the middle of a connection. Maybe limiting the ciphersuites to classical only is a better approach.

Comment on lines -10201 to +10275
if (ssl->hrr_keyshare_group != 0) {
if (ssl->hrr_keyshare_group != 0 && seenGroupsCnt > 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is an empty key share not an error? Why success on seenGroupsCnt == 0?

Comment on lines +826 to +839
#ifdef WOLFSSL_DTLS_CH_FRAG
{
/* Get the chosen group. If ret == 0 here, we are sure that the
* extension is present. */
TLSX* ksExt = TLSX_Find(parsedExts, TLSX_KEY_SHARE);
KeyShareEntry* kse = (KeyShareEntry*)ksExt->data;
if (WOLFSSL_NAMED_GROUP_IS_PQC(kse->group) ||
WOLFSSL_NAMED_GROUP_IS_PQC_HYBRID(kse->group)) {
/* Allow fragmentation of the second ClientHello due to the
* large PQC key share. */
wolfSSL_dtls13_allow_ch_frag((WOLFSSL*)ssl, 1);
}
}
#endif
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I don't know if enabling this quietly for the user is safe. We should respect the API contract we have with users where this needs to be explicitly enabled. There are DoS considerations. @rizlik

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replied above

Comment on lines +832 to +833
if (WOLFSSL_NAMED_GROUP_IS_PQC(kse->group) ||
WOLFSSL_NAMED_GROUP_IS_PQC_HYBRID(kse->group)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (WOLFSSL_NAMED_GROUP_IS_PQC(kse->group) ||
WOLFSSL_NAMED_GROUP_IS_PQC_HYBRID(kse->group)) {
if (kse != NULL && (WOLFSSL_NAMED_GROUP_IS_PQC(kse->group) ||
WOLFSSL_NAMED_GROUP_IS_PQC_HYBRID(kse->group))) {

@julek-wolfssl julek-wolfssl assigned rizlik and Frauschi and unassigned wolfSSL-Bot Feb 24, 2026
@dgarske dgarske assigned Frauschi and unassigned Frauschi Feb 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants